Next.js ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ ๊ณ ๊ธ ์์ฒญ ์์ ๊ธฐ์ ์ ์ดํด๋ณด์ธ์. ๋ณต์กํ ๋ผ์ฐํ , ์ธ์ฆ, A/B ํ ์คํธ, ํ์งํ ์ ๋ต์ ์ฒ๋ฆฌํ์ฌ ๊ฒฌ๊ณ ํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ฐ์ธ์.
Next.js ๋ฏธ๋ค์จ์ด ์ฃ์ง ์ผ์ด์ค: ์์ฒญ ์์ ํจํด ๋ง์คํฐํ๊ธฐ
Next.js ๋ฏธ๋ค์จ์ด๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ผ์ฐํธ์ ๋๋ฌํ๊ธฐ ์ ์ ์์ฒญ์ ๊ฐ๋ก์ฑ๊ณ ์์ ํ ์ ์๋ ๊ฐ๋ ฅํ ๋ฉ์ปค๋์ฆ์ ์ ๊ณตํฉ๋๋ค. ์ด ๊ธฐ๋ฅ์ ๊ฐ๋จํ ์ธ์ฆ ํ์ธ๋ถํฐ ๋ณต์กํ A/B ํ ์คํธ ์๋๋ฆฌ์ค ๋ฐ ๊ตญ์ ํ ์ ๋ต์ ์ด๋ฅด๊ธฐ๊น์ง ๊ด๋ฒ์ํ ๊ฐ๋ฅ์ฑ์ ์ด์ด์ค๋๋ค. ๊ทธ๋ฌ๋ ๋ฏธ๋ค์จ์ด๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ๋ ค๋ฉด ์ฃ์ง ์ผ์ด์ค์ ์ ์ฌ์ ์ธ ํจ์ ์ ๋ํ ๊น์ ์ดํด๊ฐ ํ์ํฉ๋๋ค. ์ด ์ข ํฉ ๊ฐ์ด๋์์๋ ๊ณ ๊ธ ์์ฒญ ์์ ํจํด์ ํ์ํ๊ณ , ๊ฒฌ๊ณ ํ๊ณ ์ฑ๋ฅ์ด ๋ฐ์ด๋ Next.js ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ๋ ๋ฐ ๋์์ด ๋๋ ์ค์ฉ์ ์ธ ์์ ์ ์คํ ๊ฐ๋ฅํ ํต์ฐฐ๋ ฅ์ ์ ๊ณตํฉ๋๋ค.
Next.js ๋ฏธ๋ค์จ์ด์ ๊ธฐ๋ณธ ์ดํดํ๊ธฐ
๊ณ ๊ธ ํจํด์ ์ดํด๋ณด๊ธฐ ์ ์ Next.js ๋ฏธ๋ค์จ์ด์ ๊ธฐ๋ณธ ์ฌํญ์ ๋ค์ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ๋ฏธ๋ค์จ์ด ํจ์๋ ์์ฒญ์ด ์๋ฃ๋๊ธฐ ์ ์ ์คํ๋์ด ๋ค์์ ์ํํ ์ ์์ต๋๋ค:
- URL ์ฌ์์ฑ: ํน์ ๊ธฐ์ค์ ๋ฐ๋ผ ์ฌ์ฉ์๋ฅผ ๋ค๋ฅธ ํ์ด์ง๋ก ๋ฆฌ๋๋ ์ ํฉ๋๋ค.
- ์ฌ์ฉ์ ๋ฆฌ๋๋ ์ : ์ฃผ๋ก ์ธ์ฆ ๋๋ ๊ถํ ๋ถ์ฌ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉ์๋ฅผ ์์ ํ ๋ค๋ฅธ URL๋ก ๋ณด๋ ๋๋ค.
- ํค๋ ์์ : HTTP ํค๋๋ฅผ ์ถ๊ฐ, ์ ๊ฑฐ ๋๋ ์ ๋ฐ์ดํธํฉ๋๋ค.
- ์ง์ ์๋ต: Next.js ๋ผ์ฐํธ๋ฅผ ์ฐํํ์ฌ ๋ฏธ๋ค์จ์ด์์ ์ง์ ์๋ต์ ๋ฐํํฉ๋๋ค.
๋ฏธ๋ค์จ์ด ํจ์๋ /pages
๋๋ /app
๋๋ ํ ๋ฆฌ(Next.js ๋ฒ์ ๋ฐ ์ค์ ์ ๋ฐ๋ผ ๋ค๋ฆ)์ middleware.js
๋๋ middleware.ts
ํ์ผ์ ์์นํฉ๋๋ค. ๋ค์ด์ค๋ ์์ฒญ์ ๋ํ๋ด๋ NextRequest
๊ฐ์ฒด๋ฅผ ๋ฐ๊ณ ํ์ ๋์์ ์ ์ดํ๊ธฐ ์ํด NextResponse
๊ฐ์ฒด๋ฅผ ๋ฐํํ ์ ์์ต๋๋ค.
์์ : ๊ธฐ๋ณธ ์ธ์ฆ ๋ฏธ๋ค์จ์ด
์ด ์์ ๋ ๊ฐ๋จํ ์ธ์ฆ ํ์ธ์ ๋ณด์ฌ์ค๋๋ค. ์ฌ์ฉ์๊ฐ ์ธ์ฆ๋์ง ์์ ๊ฒฝ์ฐ(์: ์ฟ ํค์ ์ ํจํ ํ ํฐ์ด ์๋ ๊ฒฝ์ฐ) ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋๋ ์ ๋ฉ๋๋ค.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const authToken = request.cookies.get('authToken')
if (!authToken) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/protected/:path*'],
}
์ด ๋ฏธ๋ค์จ์ด๋ /protected/:path*
์ ์ผ์นํ๋ ๋ผ์ฐํธ์ ๋ํด์๋ง ์คํ๋ฉ๋๋ค. authToken
์ฟ ํค์ ์กด์ฌ ์ฌ๋ถ๋ฅผ ํ์ธํฉ๋๋ค. ์ฟ ํค๊ฐ ์์ผ๋ฉด ์ฌ์ฉ์๋ /login
ํ์ด์ง๋ก ๋ฆฌ๋๋ ์
๋ฉ๋๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ์์ฒญ์ NextResponse.next()
๋ฅผ ์ฌ์ฉํ์ฌ ์ ์์ ์ผ๋ก ์งํ๋๋๋ก ํ์ฉ๋ฉ๋๋ค.
๊ณ ๊ธ ์์ฒญ ์์ ํจํด
์ด์ Next.js ๋ฏธ๋ค์จ์ด์ ์ง์ ํ ํ์ ๋ณด์ฌ์ฃผ๋ ๋ช ๊ฐ์ง ๊ณ ๊ธ ์์ฒญ ์์ ํจํด์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
1. ์ฟ ํค๋ฅผ ์ด์ฉํ A/B ํ ์คํธ
A/B ํ ์คํธ๋ ์ฌ์ฉ์ ๊ฒฝํ์ ์ต์ ํํ๊ธฐ ์ํ ์ค์ํ ๊ธฐ์ ์ ๋๋ค. ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์๋ฅผ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค๋ฅธ ๋ณํ์ ๋ฌด์์๋ก ํ ๋นํ๊ณ ๊ทธ๋ค์ ํ๋์ ์ถ์ ํ ์ ์์ต๋๋ค. ์ด ํจํด์ ์ฟ ํค๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์์๊ฒ ํ ๋น๋ ๋ณํ์ ์ ์งํฉ๋๋ค.
์์ : ๋๋ฉ ํ์ด์ง A/B ํ ์คํธ
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const VARIANT_A = 'variantA'
const VARIANT_B = 'variantB'
export function middleware(request: NextRequest) {
let variant = request.cookies.get('variant')?.value
if (!variant) {
// Randomly assign a variant
variant = Math.random() < 0.5 ? VARIANT_A : VARIANT_B
const response = NextResponse.next()
response.cookies.set('variant', variant)
return response
}
if (variant === VARIANT_A) {
return NextResponse.rewrite(new URL('/variant-a', request.url))
} else if (variant === VARIANT_B) {
return NextResponse.rewrite(new URL('/variant-b', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/'],
}
์ด ์์ ์์ ์ฌ์ฉ์๊ฐ ์ฒ์์ผ๋ก ๋ฃจํธ ๊ฒฝ๋ก(/
)๋ฅผ ๋ฐฉ๋ฌธํ๋ฉด ๋ฏธ๋ค์จ์ด๋ ๋ฌด์์๋ก variantA
๋๋ variantB
์ ํ ๋นํฉ๋๋ค. ์ด ๋ณํ์ ์ฟ ํค์ ์ ์ฅ๋ฉ๋๋ค. ๋์ผํ ์ฌ์ฉ์์ ํ์ ์์ฒญ์ ํ ๋น๋ ๋ณํ์ ๋ฐ๋ผ /variant-a
๋๋ /variant-b
๋ก ์ฌ์์ฑ๋ฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ค๋ฅธ ๋๋ฉ ํ์ด์ง๋ฅผ ์ ๊ณตํ๊ณ ์ด๋ค ๊ฒ์ด ๋ ๋์ ์ฑ๊ณผ๋ฅผ ๋ด๋์ง ์ถ์ ํ ์ ์์ต๋๋ค. Next.js ์ ํ๋ฆฌ์ผ์ด์
์ /variant-a
์ /variant-b
์ ๋ํ ๋ผ์ฐํธ๊ฐ ์ ์๋์ด ์๋์ง ํ์ธํ์ธ์.
์ ์ธ๊ณ์ ๊ณ ๋ ค์ฌํญ: A/B ํ ์คํธ๋ฅผ ์ํํ ๋ ์ง์ญ์ ์ฐจ์ด๋ฅผ ๊ณ ๋ คํ์ธ์. ๋ถ๋ฏธ์์ ๊ณต๊ฐ์ ์ป๋ ๋์์ธ์ด ์์์์์๋ ํจ๊ณผ์ ์ด์ง ์์ ์ ์์ต๋๋ค. IP ์ฃผ์ ์กฐํ๋ ์ฌ์ฉ์ ์ ํธ๋๋ฅผ ํตํด ์ป์ ์ง๋ฆฌ์ ์์น ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ์ง์ญ์ ๋ง๊ฒ A/B ํ ์คํธ๋ฅผ ์กฐ์ ํ ์ ์์ต๋๋ค.
2. URL ์ฌ์์ฑ์ ์ด์ฉํ ํ์งํ(i18n)
๊ตญ์ ํ(i18n)๋ ์ ์ธ๊ณ ๊ณ ๊ฐ์๊ฒ ๋๋ฌํ๋ ๋ฐ ํ์์ ์ ๋๋ค. ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์์ ์ ํธ ์ธ์ด๋ฅผ ์๋์ผ๋ก ๊ฐ์งํ๊ณ ์ฌ์ดํธ์ ์ ์ ํ ํ์งํ ๋ฒ์ ์ผ๋ก ๋ฆฌ๋๋ ์ ํ ์ ์์ต๋๋ค.
์์ : `Accept-Language` ํค๋ ๊ธฐ๋ฐ ๋ฆฌ๋๋ ์
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const SUPPORTED_LANGUAGES = ['en', 'fr', 'es', 'de']
const DEFAULT_LANGUAGE = 'en'
function getPreferredLanguage(request: NextRequest): string {
const acceptLanguage = request.headers.get('accept-language')
if (!acceptLanguage) {
return DEFAULT_LANGUAGE
}
const languages = acceptLanguage.split(',').map((lang) => lang.split(';')[0].trim())
for (const lang of languages) {
if (SUPPORTED_LANGUAGES.includes(lang)) {
return lang
}
}
return DEFAULT_LANGUAGE
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Check if there's an existing locale in the pathname
if (
SUPPORTED_LANGUAGES.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
) {
return NextResponse.next()
}
const preferredLanguage = getPreferredLanguage(request)
return NextResponse.redirect(
new URL(`/${preferredLanguage}${pathname}`, request.url)
)
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)'
],
}
์ด ๋ฏธ๋ค์จ์ด๋ ์์ฒญ์์ Accept-Language
ํค๋๋ฅผ ์ถ์ถํ์ฌ ์ฌ์ฉ์์ ์ ํธ ์ธ์ด๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. URL์ ์ด๋ฏธ ์ธ์ด ์ ๋์ฌ(์: /en/about
)๊ฐ ํฌํจ๋์ด ์์ง ์์ผ๋ฉด ๋ฏธ๋ค์จ์ด๋ ์ฌ์ฉ์๋ฅผ ์ ์ ํ ํ์งํ๋ URL(์: ํ๋์ค์ด์ ๊ฒฝ์ฐ /fr/about
)๋ก ๋ฆฌ๋๋ ์
ํฉ๋๋ค. `/pages` ๋๋ `/app` ๋๋ ํ ๋ฆฌ์ ๋ค๋ฅธ ๋ก์ผ์ผ์ ๋ง๋ ์ ์ ํ ํด๋ ๊ตฌ์กฐ๊ฐ ์๋์ง ํ์ธํ์ธ์. ์๋ฅผ ๋ค์ด, `/pages/en/about.js`์ `/pages/fr/about.js` ํ์ผ์ด ํ์ํฉ๋๋ค.
์ ์ธ๊ณ์ ๊ณ ๋ ค์ฌํญ: i18n ๊ตฌํ์ด ์ค๋ฅธ์ชฝ์์ ์ผ์ชฝ์ผ๋ก ์ฐ๋ ์ธ์ด(์: ์๋์ด, ํ๋ธ๋ฆฌ์ด)๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ฒ๋ฆฌํ๋์ง ํ์ธํ์ธ์. ๋ํ ์ฝํ ์ธ ์ ์ก ๋คํธ์ํฌ(CDN)๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์์๊ฒ ๋ ๊ฐ๊น์ด ์๋ฒ์์ ํ์งํ๋ ์์ฐ์ ์ ๊ณตํ์ฌ ์ฑ๋ฅ์ ํฅ์์ํค๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.
3. ๊ธฐ๋ฅ ํ๋๊ทธ
๊ธฐ๋ฅ ํ๋๊ทธ๋ฅผ ์ฌ์ฉํ๋ฉด ์ ์ฝ๋๋ฅผ ๋ฐฐํฌํ์ง ์๊ณ ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ธฐ๋ฅ์ ํ์ฑํํ๊ฑฐ๋ ๋นํ์ฑํํ ์ ์์ต๋๋ค. ์ด๋ ์๋ก์ด ๊ธฐ๋ฅ์ ์ ์ง์ ์ผ๋ก ์ถ์ํ๊ฑฐ๋ ํ๋ก๋์ ํ๊ฒฝ์์ ๊ธฐ๋ฅ์ ํ ์คํธํ๋ ๋ฐ ํนํ ์ ์ฉํฉ๋๋ค. ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ์ฌ ๊ธฐ๋ฅ ํ๋๊ทธ์ ์ํ๋ฅผ ํ์ธํ๊ณ ๊ทธ์ ๋ฐ๋ผ ์์ฒญ์ ์์ ํ ์ ์์ต๋๋ค.
์์ : ๋ฒ ํ ๊ธฐ๋ฅ ํ์ฑํ
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const BETA_FEATURE_ENABLED = process.env.BETA_FEATURE_ENABLED === 'true'
export function middleware(request: NextRequest) {
if (BETA_FEATURE_ENABLED && request.nextUrl.pathname.startsWith('/new-feature')) {
return NextResponse.next()
}
// Optionally redirect to a "feature unavailable" page
return NextResponse.rewrite(new URL('/feature-unavailable', request.url))
}
export const config = {
matcher: ['/new-feature/:path*'],
}
์ด ๋ฏธ๋ค์จ์ด๋ BETA_FEATURE_ENABLED
ํ๊ฒฝ ๋ณ์์ ๊ฐ์ ํ์ธํฉ๋๋ค. ์ด ๊ฐ์ด true
๋ก ์ค์ ๋์ด ์๊ณ ์ฌ์ฉ์๊ฐ /new-feature
์๋์ ๋ผ์ฐํธ์ ์ก์ธ์คํ๋ ค๊ณ ํ๋ฉด ์์ฒญ์ด ์งํ๋๋๋ก ํ์ฉ๋ฉ๋๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ์ฌ์ฉ์๋ /feature-unavailable
ํ์ด์ง๋ก ๋ฆฌ๋๋ ์
๋ฉ๋๋ค. ๊ฐ๋ฐ, ์คํ
์ด์ง, ํ๋ก๋์
๋ฑ ๋ค์ํ ํ๊ฒฝ์ ๋ง๊ฒ ํ๊ฒฝ ๋ณ์๋ฅผ ์ ์ ํ๊ฒ ๊ตฌ์ฑํด์ผ ํฉ๋๋ค.
์ ์ธ๊ณ์ ๊ณ ๋ ค์ฌํญ: ๊ธฐ๋ฅ ํ๋๊ทธ๋ฅผ ์ฌ์ฉํ ๋ ๋ชจ๋ ์ง์ญ์ ๊ท์ ์ ์ค์ํ์ง ์์ ์ ์๋ ๊ธฐ๋ฅ์ ํ์ฑํํ๋ ๊ฒ์ ๋ฒ์ ์ํฅ์ ๊ณ ๋ คํ์ธ์. ์๋ฅผ ๋ค์ด, ๋ฐ์ดํฐ ํ๋ผ์ด๋ฒ์์ ๊ด๋ จ๋ ๊ธฐ๋ฅ์ ํน์ ๊ตญ๊ฐ์์ ๋นํ์ฑํํด์ผ ํ ์ ์์ต๋๋ค.
4. ์ฅ์น ๊ฐ์ง ๋ฐ ์ ์ํ ๋ผ์ฐํ
ํ๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ํ ํ๋ฉด ํฌ๊ธฐ์ ์ฅ์น ๊ธฐ๋ฅ์ ๋ฐ์ํ๊ณ ์ ์ํด์ผ ํฉ๋๋ค. ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์์ ์ฅ์น ์ ํ์ ๊ฐ์งํ๊ณ ์ฌ์ดํธ์ ์ต์ ํ๋ ๋ฒ์ ์ผ๋ก ๋ฆฌ๋๋ ์ ํ ์ ์์ต๋๋ค.
์์ : ๋ชจ๋ฐ์ผ ์ฌ์ฉ์๋ฅผ ๋ชจ๋ฐ์ผ ์ต์ ํ ํ์ ๋๋ฉ์ธ์ผ๋ก ๋ฆฌ๋๋ ์
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { device } from 'detection'
export function middleware(request: NextRequest) {
const userAgent = request.headers.get('user-agent')
if (userAgent) {
const deviceType = device(userAgent)
if (deviceType.type === 'phone') {
const mobileUrl = new URL(request.url)
mobileUrl.hostname = 'm.example.com'
return NextResponse.redirect(mobileUrl)
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/'],
}
์ด ์์ ๋ `detection` ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ User-Agent
ํค๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ์์ ์ฅ์น ์ ํ์ ๊ฒฐ์ ํฉ๋๋ค. ์ฌ์ฉ์๊ฐ ํด๋ํฐ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ m.example.com
ํ์ ๋๋ฉ์ธ์ผ๋ก ๋ฆฌ๋๋ ์
๋ฉ๋๋ค(๋ชจ๋ฐ์ผ ์ต์ ํ ๋ฒ์ ์ ์ฌ์ดํธ๊ฐ ํด๋น ์์น์ ํธ์คํ
๋์ด ์๋ค๊ณ ๊ฐ์ ). `detection` ํจํค์ง๋ฅผ ์ค์นํด์ผ ํฉ๋๋ค: `npm install detection`.
์ ์ธ๊ณ์ ๊ณ ๋ ค์ฌํญ: ์ฅ์น ๊ฐ์ง ๋ก์ง์ด ์ฅ์น ์ฌ์ฉ์ ์ง์ญ์ ์ฐจ์ด๋ฅผ ๊ณ ๋ คํ๋์ง ํ์ธํ์ธ์. ์๋ฅผ ๋ค์ด, ํผ์ฒํฐ์ ์ผ๋ถ ๊ฐ๋ฐ๋์๊ตญ์์ ์ฌ์ ํ ๋๋ฆฌ ์ฌ์ฉ๋ฉ๋๋ค. ๋ ๊ฐ๋ ฅํ ์๋ฃจ์ ์ ์ํด User-Agent ๊ฐ์ง์ ๋ฐ์ํ ๋์์ธ ๊ธฐ์ ์ ์กฐํฉํ์ฌ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.
5. ์์ฒญ ํค๋ ๋ณด๊ฐ
๋ฏธ๋ค์จ์ด๋ ์ ํ๋ฆฌ์ผ์ด์ ๋ผ์ฐํธ์์ ์ฒ๋ฆฌ๋๊ธฐ ์ ์ ์์ฒญ ํค๋์ ์ ๋ณด๋ฅผ ์ถ๊ฐํ ์ ์์ต๋๋ค. ์ด๋ ์ฌ์ฉ์ ์ญํ , ์ธ์ฆ ์ํ ๋๋ ์์ฒญ ID์ ๊ฐ์ ์ฌ์ฉ์ ์ ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ๋ ๋ฐ ์ ์ฉํ๋ฉฐ, ์ด๋ ์ ํ๋ฆฌ์ผ์ด์ ๋ก์ง์์ ์ฌ์ฉ๋ ์ ์์ต๋๋ค.
์์ : ์์ฒญ ID ์ถ๊ฐ
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
export function middleware(request: NextRequest) {
const requestId = uuidv4()
const response = NextResponse.next()
response.headers.set('x-request-id', requestId)
return response
}
export const config = {
matcher: ['/api/:path*'], // Only apply to API routes
}
์ด ๋ฏธ๋ค์จ์ด๋ uuid
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊ณ ์ ํ ์์ฒญ ID๋ฅผ ์์ฑํ๊ณ x-request-id
ํค๋์ ์ถ๊ฐํฉ๋๋ค. ์ด ID๋ ๋ก๊น
, ์ถ์ ๋ฐ ๋๋ฒ๊น
๋ชฉ์ ์ผ๋ก ์ฌ์ฉ๋ ์ ์์ต๋๋ค. uuid
ํจํค์ง๋ฅผ ์ค์นํด์ผ ํฉ๋๋ค: `npm install uuid`.
์ ์ธ๊ณ์ ๊ณ ๋ ค์ฌํญ: ์ฌ์ฉ์ ์ ์ ํค๋๋ฅผ ์ถ๊ฐํ ๋ ํค๋ ํฌ๊ธฐ ์ ํ์ ์ ์ํ์ธ์. ์ด ์ ํ์ ์ด๊ณผํ๋ฉด ์๊ธฐ์น ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ํ ํค๋์ ์ถ๊ฐ๋ ๋ฏผ๊ฐํ ์ ๋ณด๊ฐ ์ ๋๋ก ๋ณดํธ๋๋์ง ํ์ธํ์ธ์. ํนํ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ฆฌ๋ฒ์ค ํ๋ก์๋ CDN ๋ค์ ์๋ ๊ฒฝ์ฐ ๋์ฑ ๊ทธ๋ ์ต๋๋ค.
6. ๋ณด์ ๊ฐํ: ์๋ ์ ํ
๋ฏธ๋ค์จ์ด๋ ์๋ ์ ํ์ ๊ตฌํํ์ฌ ์ ์์ ์ธ ๊ณต๊ฒฉ์ ๋ํ ์ฒซ ๋ฒ์งธ ๋ฐฉ์ด์ ์ญํ ์ ํ ์ ์์ต๋๋ค. ์ด๋ ํด๋ผ์ด์ธํธ๊ฐ ํน์ ์๊ฐ ๋ด์ ํ ์ ์๋ ์์ฒญ ์๋ฅผ ์ ํํ์ฌ ๋จ์ฉ์ ๋ฐฉ์งํฉ๋๋ค.
์์ : ๊ฐ๋จํ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ ๊ธฐ๋ณธ ์๋ ์ ํ
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const requestCounts: { [ip: string]: number } = {}
const WINDOW_SIZE_MS = 60000; // 1 minute
const MAX_REQUESTS_PER_WINDOW = 100;
export function middleware(request: NextRequest) {
const clientIP = request.ip || '127.0.0.1' // Get client IP, default to localhost for local testing
if (!requestCounts[clientIP]) {
requestCounts[clientIP] = 0;
}
requestCounts[clientIP]++;
if (requestCounts[clientIP] > MAX_REQUESTS_PER_WINDOW) {
return new NextResponse(
JSON.stringify({ message: 'Too many requests' }),
{ status: 429, headers: { 'Content-Type': 'application/json' } }
);
}
// Reset count after window
setTimeout(() => {
requestCounts[clientIP]--;
if (requestCounts[clientIP] <= 0) {
delete requestCounts[clientIP];
}
}, WINDOW_SIZE_MS);
return NextResponse.next();
}
export const config = {
matcher: ['/api/:path*'], // Apply to all API routes
}
์ด ์์ ๋ ๊ฐ๋จํ ์ธ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์(requestCounts
)๋ฅผ ์ ์งํ์ฌ ๊ฐ IP ์ฃผ์์ ์์ฒญ ์๋ฅผ ์ถ์ ํฉ๋๋ค. ํด๋ผ์ด์ธํธ๊ฐ WINDOW_SIZE_MS
๋ด์์ MAX_REQUESTS_PER_WINDOW
๋ฅผ ์ด๊ณผํ๋ฉด ๋ฏธ๋ค์จ์ด๋ 429 Too Many Requests
์ค๋ฅ๋ฅผ ๋ฐํํฉ๋๋ค. ์ค์: ์ด๊ฒ์ ๋จ์ํ๋ ์์ ์ด๋ฉฐ ํ์ฅ๋์ง ์๊ณ ์๋น์ค ๊ฑฐ๋ถ ๊ณต๊ฒฉ์ ์ทจ์ฝํ๊ธฐ ๋๋ฌธ์ ํ๋ก๋์
ํ๊ฒฝ์๋ ์ ํฉํ์ง ์์ต๋๋ค. ํ๋ก๋์
์ฉ๋๋ก๋ Redis๋ ์ ์ฉ ์๋ ์ ํ ์๋น์ค์ ๊ฐ์ ๋ ๊ฐ๋ ฅํ ์๋ ์ ํ ์๋ฃจ์
์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.
์ ์ธ๊ณ์ ๊ณ ๋ ค์ฌํญ: ์๋ ์ ํ ์ ๋ต์ ์ ํ๋ฆฌ์ผ์ด์ ์ ํน์ ํน์ฑ๊ณผ ์ฌ์ฉ์์ ์ง๋ฆฌ์ ๋ถํฌ์ ๋ง๊ฒ ์กฐ์ ๋์ด์ผ ํฉ๋๋ค. ๋ค๋ฅธ ์ง์ญ์ด๋ ์ฌ์ฉ์ ์ธ๊ทธ๋จผํธ์ ๋ํด ๋ค๋ฅธ ์๋ ์ ํ์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.
์ฃ์ง ์ผ์ด์ค์ ์ ์ฌ์ ํจ์
๋ฏธ๋ค์จ์ด๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ด์ง๋ง, ๊ทธ ํ๊ณ์ ์ ์ฌ์ ์ธ ํจ์ ์ ์ธ์งํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค:
- ์ฑ๋ฅ ์ํฅ: ๋ฏธ๋ค์จ์ด๋ ๋ชจ๋ ์์ฒญ์ ์ค๋ฒํค๋๋ฅผ ์ถ๊ฐํฉ๋๋ค. ๋ฏธ๋ค์จ์ด์์ ๊ณ์ฐ ๋น์ฉ์ด ๋ง์ด ๋๋ ์์ ์ ์ํํ๋ฉด ์ฑ๋ฅ์ ์ฌ๊ฐํ ์ํฅ์ ์ค ์ ์์ผ๋ฏ๋ก ํผํ์ธ์. ๋ฏธ๋ค์จ์ด๋ฅผ ํ๋กํ์ผ๋งํ์ฌ ์ฑ๋ฅ ๋ณ๋ชฉ ํ์์ ์๋ณํ๊ณ ์ต์ ํํ์ธ์.
- ๋ณต์ก์ฑ: ๋ฏธ๋ค์จ์ด๋ฅผ ๊ณผ๋ํ๊ฒ ์ฌ์ฉํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ ์ดํดํ๊ณ ์ ์ง ๊ด๋ฆฌํ๊ธฐ๊ฐ ๋ ์ด๋ ค์์ง ์ ์์ต๋๋ค. ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์คํ๊ฒ ์ฌ์ฉํ๊ณ ๊ฐ ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ด ๋ช ํํ๊ณ ์ ์ ์๋ ๋ชฉ์ ์ ๊ฐ๋๋ก ํ์ธ์.
- ํ ์คํ : ๋ฏธ๋ค์จ์ด ํ ์คํธ๋ HTTP ์์ฒญ์ ์๋ฎฌ๋ ์ด์ ํ๊ณ ๊ฒฐ๊ณผ ์๋ต์ ๊ฒ์ฌํด์ผ ํ๋ฏ๋ก ์ด๋ ค์ธ ์ ์์ต๋๋ค. Jest ๋ฐ Supertest์ ๊ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ ๋ํ ํฌ๊ด์ ์ธ ๋จ์ ๋ฐ ํตํฉ ํ ์คํธ๋ฅผ ์์ฑํ์ธ์.
- ์ฟ ํค ๊ด๋ฆฌ: ๋ฏธ๋ค์จ์ด์์ ์ฟ ํค๋ฅผ ์ค์ ํ ๋๋ ์บ์ฑ ๋์์ ์ํฅ์ ์ค ์ ์์ผ๋ฏ๋ก ์ฃผ์ํด์ผ ํฉ๋๋ค. ์ฟ ํค ๊ธฐ๋ฐ ์บ์ฑ์ ์ํฅ์ ์ดํดํ๊ณ ์บ์ ํค๋๋ฅผ ๊ทธ์ ๋ง๊ฒ ๊ตฌ์ฑํ์ธ์.
- ํ๊ฒฝ ๋ณ์: ๋ฏธ๋ค์จ์ด์์ ์ฌ์ฉ๋๋ ๋ชจ๋ ํ๊ฒฝ ๋ณ์๊ฐ ๊ฐ๋ฐ, ์คํ ์ด์ง, ํ๋ก๋์ ๋ฑ ๋ค์ํ ํ๊ฒฝ์ ๋ง๊ฒ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์๋์ง ํ์ธํ์ธ์. Dotenv์ ๊ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ํ๊ฒฝ ๋ณ์๋ฅผ ๊ด๋ฆฌํ์ธ์.
- ์ฃ์ง ํจ์ ์ ํ: ๋ฏธ๋ค์จ์ด๋ ์ฃ์ง ํจ์๋ก ์คํ๋๋ฉฐ ์คํ ์๊ฐ, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ฐ ๋ฒ๋ค ์ฝ๋ ํฌ๊ธฐ์ ์ ํ์ด ์์์ ๊ธฐ์ตํ์ธ์. ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ ๊ฐ๋ณ๊ณ ํจ์จ์ ์ผ๋ก ์ ์งํ์ธ์.
Next.js ๋ฏธ๋ค์จ์ด ์ฌ์ฉ์ ์ํ ๋ชจ๋ฒ ์ฌ๋ก
Next.js ๋ฏธ๋ค์จ์ด์ ์ด์ ์ ๊ทน๋ํํ๊ณ ์ ์ฌ์ ์ธ ๋ฌธ์ ๋ฅผ ํผํ๋ ค๋ฉด ๋ค์ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ฐ๋ฅด์ธ์:
- ๋จ์ํ๊ฒ ์ ์งํ๊ธฐ: ๊ฐ ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ ๋จ์ผํ๊ณ ์ ์ ์๋ ์ฑ ์์ ๊ฐ์ ธ์ผ ํฉ๋๋ค. ์ฌ๋ฌ ์์ ์ ์ํํ๋ ์ง๋์น๊ฒ ๋ณต์กํ ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ ๋ง๋ค์ง ๋ง์ธ์.
- ์ฑ๋ฅ ์ต์ ํ: ์ฑ๋ฅ ๋ณ๋ชฉ ํ์์ ํผํ๊ธฐ ์ํด ๋ฏธ๋ค์จ์ด์์ ์ํ๋๋ ์ฒ๋ฆฌ๋์ ์ต์ํํ์ธ์. ์บ์ฑ ์ ๋ต์ ์ฌ์ฉํ์ฌ ๋ฐ๋ณต์ ์ธ ๊ณ์ฐ์ ํ์์ฑ์ ์ค์ด์ธ์.
- ์ฒ ์ ํ๊ฒ ํ ์คํธํ๊ธฐ: ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ด ์์๋๋ก ์๋ํ๋์ง ํ์ธํ๊ธฐ ์ํด ํฌ๊ด์ ์ธ ๋จ์ ๋ฐ ํตํฉ ํ ์คํธ๋ฅผ ์์ฑํ์ธ์.
- ์ฝ๋ ๋ฌธ์ํ: ์ ์ง ๊ด๋ฆฌ์ฑ์ ํฅ์์ํค๊ธฐ ์ํด ๊ฐ ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ ๋ชฉ์ ๊ณผ ๊ธฐ๋ฅ์ ๋ช ํํ๊ฒ ๋ฌธ์ํํ์ธ์.
- ์ ํ๋ฆฌ์ผ์ด์ ๋ชจ๋ํฐ๋ง: ๋ชจ๋ํฐ๋ง ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ ์ฑ๋ฅ ๋ฐ ์ค๋ฅ์จ์ ์ถ์ ํ์ธ์.
- ์คํ ์์ ์ดํดํ๊ธฐ: ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ด ์คํ๋๋ ์์๋ฅผ ์ธ์งํ์ธ์. ์ด๋ ๋์์ ์ํฅ์ ์ค ์ ์์ต๋๋ค.
- ํ๊ฒฝ ๋ณ์ ํ๋ช ํ๊ฒ ์ฌ์ฉํ๊ธฐ: ํ๊ฒฝ ๋ณ์๋ฅผ ์ฌ์ฉํ์ฌ ๋ค์ํ ํ๊ฒฝ์ ๋ง๊ฒ ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ ๊ตฌ์ฑํ์ธ์.
๊ฒฐ๋ก
Next.js ๋ฏธ๋ค์จ์ด๋ ์ฃ์ง์์ ์์ฒญ์ ์์ ํ๊ณ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋์์ ์ฌ์ฉ์ ์ ์ํ ์ ์๋ ๊ฐ๋ ฅํ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ์ด ๊ฐ์ด๋์์ ๋ ผ์๋ ๊ณ ๊ธ ์์ฒญ ์์ ํจํด์ ์ดํดํจ์ผ๋ก์จ ๊ฒฌ๊ณ ํ๊ณ ์ฑ๋ฅ์ด ๋ฐ์ด๋๋ฉฐ ์ ์ธ๊ณ์ ์ผ๋ก ์ธ์๋๋ Next.js ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ ์ ์์ต๋๋ค. ์ฃ์ง ์ผ์ด์ค์ ์ ์ฌ์ ์ธ ํจ์ ์ ์ ์คํ๊ฒ ๊ณ ๋ คํ๊ณ , ์์ ์ค๋ช ๋ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ฐ๋ผ ๋ฏธ๋ค์จ์ด ๊ธฐ๋ฅ์ด ์ ๋ขฐํ ์ ์๊ณ ์ ์ง ๊ด๋ฆฌ ๊ฐ๋ฅํ๋๋ก ํ์ธ์. ๋ฏธ๋ค์จ์ด์ ํ์ ๋ฐ์๋ค์ฌ ํ์ํ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ง๋ค๊ณ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์๋ก์ด ๊ฐ๋ฅ์ฑ์ ์ด์ด๋ณด์ธ์.